using System;
using System.Xml;
using System.IO;
using System.Xml.XPath;

namespace Microsoft.Health.Samples.PatientConnect.TreyResearch.HealthVaultAdapter
{
    /// <summary>
    /// This class parses HL7 message and generates an xml file
    /// As per ELINCS standard this parser generates the xml equivalent for ORU segments only 
    /// </summary>
    public class HL7MessageParser
    {
        private const string _xmlNodeDelimiter = ".";
        private const string _nteSegment = "nte";
        private const string _obxSegment = "obx";
        private const char _fieldDelimiter = '^';
        private const char _componentDelimiter = '&';
        private const string _escCompValue = @"\&";
        private const string _messageEncodingChar = "^~\\&";
        private const string _segmentDelimiter = "|";
        private const string _rootElementHeader = "HL7";

        XmlDocument _xmlDoc;
        XmlElement _rootElement;
        XmlElement _parentElement;
        XmlElement _componentElement;
        XmlElement _subComponentElement;
        XmlElement _messageElement;

        int _fieldLength = 1;
        int _componentCount = 1;
        int _SubComponentCount = 1;

        /// <summary>
        /// Parse HL7 Message segment and generate the xml file
        /// </summary>
        /// <param name="hl7MessagePath">File path of HL7 message</param>
        /// <returns>XmlDocument</returns>
        public XmlDocument ParseFile(string hl7MessageFilePath)
        {
            //Start writing xml file
            _xmlDoc = new XmlDocument();

            CreateRootElement();

            string[] messageSegments = File.ReadAllLines(hl7MessageFilePath);
            foreach (String messageSegment in messageSegments)
            {
                //Reinitialize the value for each message segment
                _fieldLength = 1;
                _componentCount = 1;
                _SubComponentCount = 1;
                //Split the message segment for fields
                string[] messageFields = messageSegment.Split(Convert.ToChar(_segmentDelimiter));
                _parentElement = NestObrSegment(_xmlDoc, messageFields[0]);

                for (int fieldIndex = 1; fieldIndex < messageFields.Length; fieldIndex++)
                {
                    CreateMessageXmlFile(messageFields, fieldIndex);
                }
            }
            return _xmlDoc;
        }

        /// <summary>
        /// Create Xml equivalent of HL7 message
        /// </summary>
        /// <param name="messageFields">Message field segments</param>
        /// <param name="fieldIndex">current field index</param>
        private void CreateMessageXmlFile(string[] messageFields,
                                          int fieldIndex)
        {
            _messageElement = _xmlDoc.CreateElement(messageFields[0] + _xmlNodeDelimiter + _fieldLength);
            _parentElement.AppendChild(_messageElement);
            if (!messageFields[fieldIndex].Equals(_messageEncodingChar))
            {
                if (messageFields[fieldIndex].Contains(_fieldDelimiter.ToString()) //Check for field in message
                    || (messageFields[fieldIndex].Contains(_componentDelimiter.ToString()) // check component in message
                    && !(messageFields[fieldIndex].Contains(_escCompValue))) // check for \& in message
                    )
                {
                    CreateComponent(messageFields[fieldIndex], messageFields[0]);
                }
                else
                {
                    _messageElement.InnerText = messageFields[fieldIndex];
                }
            }
            else
            {
                string xmlNodeName = messageFields[0];
                string xmlNodeValue = messageFields[fieldIndex];
                CreateFieldSeparatorNode(_xmlDoc, _parentElement, _messageElement, xmlNodeName, xmlNodeValue);
            }
            _fieldLength++;
        }

        /// <summary>
        /// Creates Component for entire HL7 message
        /// </summary>
        /// <param name="messageFields">message fields</param>
        /// <param name="xmlNodeName">xml node name</param>
        private void CreateComponent(string messageFields,
                                     string xmlNodeName)
        {
            string[] messageComponents = messageFields.Split(_fieldDelimiter);
            _componentCount = 1;
            foreach (string component in messageComponents)
            {
                _componentElement = CreateComponent(_xmlDoc, _messageElement, xmlNodeName);
                //Check for subComponent
                if (component.Contains(_componentDelimiter.ToString()))
                {
                    _componentCount--;
                    _SubComponentCount = 1;
                    //Split the message component for subcomponent
                    string[] messageSubcomponents = component.Split(_componentDelimiter);
                    foreach (string subcomponent in messageSubcomponents)
                    {
                        _subComponentElement = CreateSubComponent(_xmlDoc, _componentElement, xmlNodeName, subcomponent);
                    }
                }
                else
                {
                    _componentElement.InnerText = component;
                }
            }
        }

        /// <summary>
        /// Creates the Root element for the message
        /// value : HL7
        /// </summary>
        private void CreateRootElement()
        {
            XmlDeclaration xmlDeclaration = _xmlDoc.CreateXmlDeclaration("1.0", System.Text.Encoding.UTF8.BodyName, "no");
            _rootElement = _xmlDoc.CreateElement(_rootElementHeader);
            _xmlDoc.InsertBefore(xmlDeclaration, _xmlDoc.DocumentElement);
            _xmlDoc.AppendChild(_rootElement);
        }

        /// <summary>
        /// Nest OBX, NTE in OBR segment
        /// </summary>
        /// <param name="xmlDoc">Xml document</param>
        /// <param name="messageFields">message Fields</param>
        /// <returns></returns>
        private XmlElement NestObrSegment(XmlDocument xmlDoc,
                                          string xmlNodeName)
        {
            XmlElement parentElement;
            if (xmlNodeName.ToLower().Equals(_obxSegment) || xmlNodeName.ToLower().Equals(_nteSegment))
            {
                parentElement = xmlDoc.CreateElement(xmlNodeName);
                XPathNavigator navigator = xmlDoc.CreateNavigator();
                XPathNodeIterator iterator = navigator.Select("HL7/OBR");
                xmlDoc.SelectSingleNode("HL7/OBR[" + iterator.Count + "]").AppendChild(parentElement);
            }
            else
            {
                parentElement = xmlDoc.CreateElement(xmlNodeName);
                xmlDoc.DocumentElement.AppendChild(parentElement);
            }
            return parentElement;
        }

        /// <summary>
        /// Create Node containing special character such as Field Separator (|)
        /// </summary>
        /// <param name="xmlDoc">Xml document</param>
        /// <param name="parentElement">Parent Element of the node</param>
        /// <param name="messageElement">current message element</param>
        /// <param name="xmlNodeName">Xml node name</param>
        /// <param name="xmlNodeValue">Xml node value</param>
        private void CreateFieldSeparatorNode(XmlDocument xmlDoc,
                                             XmlElement parentElement,
                                             XmlElement messageElement,
                                             string xmlNodeName,
                                             string xmlNodeValue)
        {
            messageElement.InnerText = _segmentDelimiter;
            _fieldLength++;
            messageElement = xmlDoc.CreateElement(xmlNodeName + _xmlNodeDelimiter + _fieldLength);
            messageElement.InnerText = xmlNodeValue;
            parentElement.AppendChild(messageElement);
            _fieldLength++;
        }

        /// <summary>
        /// Create component as per HL7 message
        /// </summary>
        /// <param name="xmlDoc">Xml document</param>
        /// <param name="messageElement">current message element</param>
        /// <param name="messageFields">Message Fields</param>
        /// <returns>Xml element</returns>
        private XmlElement CreateComponent(XmlDocument xmlDoc,
                                           XmlElement messageElement,
                                           string xmlNodeName)
        {
            XmlElement componentElement;
            componentElement = xmlDoc.CreateElement(xmlNodeName + _xmlNodeDelimiter + _fieldLength + _xmlNodeDelimiter + _componentCount);
            messageElement.AppendChild(componentElement);
            _componentCount++;
            return componentElement;
        }

        /// <summary>
        /// Create sub component as per HL7 message
        /// </summary>
        /// <param name="xmlDoc">xml document</param>
        /// <param name="componentElement">Parent component element</param>
        /// <param name="messageFields">Message fields</param>
        /// <param name="subcomponent">subcomponent value</param>
        /// <returns>Xml element</returns>
        private XmlElement CreateSubComponent(XmlDocument xmlDoc,
                                             XmlElement componentElement,
                                             string xmlNodeName,
                                             string subcomponent)
        {
            XmlElement subComponentElement;
            subComponentElement = xmlDoc.CreateElement(xmlNodeName + _xmlNodeDelimiter + _fieldLength
                + _xmlNodeDelimiter + _componentCount + _xmlNodeDelimiter + _SubComponentCount);

            subComponentElement.InnerText = subcomponent;
            componentElement.AppendChild(subComponentElement);
            _SubComponentCount++;
            return subComponentElement;
        }
    }
}